import {
  CMEVENTS,
  DocumentObject,
  IAssemblyDocTreeNode,
  ShapeChangePendingEvent,
  Types,
} from '@amcax/base';
import { loadEmit, loadProgressEmit } from '@app-base/common/events/CoreEvent';
import { COMMON_EVENTS, PRODUCT_EVENT } from '@app-cad/common/Constants';
import { ctx2, referenceManager, userEvents } from '@app-cad/module';
import { throttle } from 'lodash';
import { Dispatch, SetStateAction } from 'react';
import { measureRenderManager } from '../../manager/MeasureRenderManager';
import {
  addMeasurementNode,
  applyNodesToTree,
  countTotalPartsNumber,
  createMeasurementNode,
  gatherSubPartsOfRootChildren,
  getDocsInfo,
  loadRefDoc,
  measurementNodes,
  measurementRootNode,
  refDocNodes,
  removeRenderedRefDoc,
  unsortedTreeNodes,
} from './assembly-funcs';
import { addEntireGeomBox } from './assembly-geomBox-controller';

let assemblyRendered = false;

userEvents.on(PRODUCT_EVENT.ASSEMBLY_RENDERED, () => (assemblyRendered = true));

let documentInfo: CadDocument.DocumentInfo;

userEvents.on(
  COMMON_EVENTS.DOCUMENT_INFO_UPDATED,
  (docInfo) => (documentInfo = docInfo),
);

const handleUpdate = async (
  docTree: IAssemblyDocTreeNode,
  updatedRefDocDxidArray: string[],
) => {
  const { objects: allObjects } =
    await ctx2.commandCenters.basicCommands.getObjects();

  const objects = allObjects?.filter((obj) =>
    updatedRefDocDxidArray.includes(obj.dxid),
  );

  const hasMeasureResult = objects?.some(
    ({ type }) => type === Types.MeasureObject,
  );

  const affectedMeasurementKeys = updatedRefDocDxidArray
    .map(
      (dxid) =>
        [...measurementNodes.values()].find((node) => {
          const { dxid1 = '', dxid2 = '' } = (node.originalObj as any).params;

          return dxid1.startsWith(dxid) || dxid2.startsWith(dxid);
        })?.key,
    )
    .filter(Boolean);

  measureRenderManager.removeMeasureResult(affectedMeasurementKeys);

  objects?.forEach((docObj: DocumentObject) => {
    countTotalPartsNumber(docObj);
  });

  // while previewing array objects, there could be response without objects by getObjects
  objects?.forEach((docObj: DocumentObject) => {
    const refDocNode = refDocNodes.get(docObj.dxid);

    if (refDocNode) {
      const parentNode = refDocNode.parent;
      const index = parentNode.children.findIndex(
        (child) => child.key === refDocNode.key,
      );

      if (index !== -1) {
        removeRenderedRefDoc(refDocNode);

        parentNode.children[index] = loadRefDoc(
          docObj,
          parentNode,
          refDocNode.collectionUUID,
        );

        refDocNodes.set(docObj.dxid, parentNode.children[index]);
      }
    }

    const measurementNode = measurementNodes.get(docObj.dxid);
    if (measurementNode) {
      const parentNode = measurementNode.parent;
      const index = parentNode.children.findIndex(
        (child) => child.key === measurementNode.key,
      );

      if (index !== -1) {
        parentNode.children[index] = createMeasurementNode(docObj);
        measurementNodes.set(docObj.dxid, parentNode.children[index]);

        measureRenderManager.setVisibility(docObj.dxid, docObj.visibility);
      }
    }

    if (docObj.type === Types.GeomBox) {
      addEntireGeomBox(docObj, { ...docTree, documentInfo });
    }
  });

  if (affectedMeasurementKeys?.length || hasMeasureResult) {
    await measureRenderManager.rerenderMeasureResults();
  }

  const allObjectKeys = allObjects.map((obj: DocumentObject) => obj.dxid);
  const rootTreeChildrenKeys = docTree.children.map((child) => child.key);

  // Check for possible move up/down or swap if 2 objects are updated
  if (updatedRefDocDxidArray.length === 2) {
    const [dxidA, dxidB] = updatedRefDocDxidArray;
    const idxAllA = allObjectKeys.indexOf(dxidA);
    const idxAllB = allObjectKeys.indexOf(dxidB);
    const idxTreeA = rootTreeChildrenKeys.indexOf(dxidA);
    const idxTreeB = rootTreeChildrenKeys.indexOf(dxidB);
    if (
      idxAllA !== -1 &&
      idxAllB !== -1 &&
      idxTreeA !== -1 &&
      idxTreeB !== -1
    ) {
      // if the order in allObjects is different from the order in docTree.children,
      if (idxAllA < idxAllB !== idxTreeA < idxTreeB) {
        const nodeA = docTree.children[idxTreeA];
        const nodeB = docTree.children[idxTreeB];
        docTree.children[idxTreeA] = nodeB;
        docTree.children[idxTreeB] = nodeA;
      }
    }
  }
};

const handleAdded = throttle(
  async (docTree: IAssemblyDocTreeNode, added: string[]) => {
    const { objects } = await ctx2.commandCenters.basicCommands.getObjects();

    const hasMeasureResult = objects?.some(
      ({ type }) => type === Types.MeasureObject,
    );

    objects?.forEach((docObj: DocumentObject) => {
      countTotalPartsNumber(docObj, added);
    });

    // for unknown reason, sometimes there could be response without objects by getObjects
    // especially for a new product document
    objects?.forEach((docObj: DocumentObject) => {
      if (docObj.type === Types.MeasureObject) {
        const measurementChildrenKeySet = new Set(
          measurementRootNode?.children.map(({ key }) => key),
        );

        if (!measurementChildrenKeySet.has(docObj.dxid)) {
          addMeasurementNode(docTree, docObj);
        }
      } else if (docObj.type === Types.GeomBox) {
        addEntireGeomBox(docObj, { ...docTree, documentInfo });
      } else if (docObj.assembly != null) {
        // deal with array edit first
        if (docObj.assembly.asmtype === 'array') {
          const arrayDocNode = docTree.children.find(
            ({ key }) => key === docObj.dxid,
          );

          // no arrayDocNode means the array is new, not edited
          if (arrayDocNode) {
            arrayDocNode.assembly = docObj.assembly;
            arrayDocNode.originalObj = docObj;

            added.forEach((addedItem) => {
              const addedItemDoc = docObj.children.find(
                ({ dxid }) => dxid === addedItem,
              );
              if (addedItemDoc) {
                arrayDocNode.children.push(
                  loadRefDoc(
                    addedItemDoc,
                    arrayDocNode,
                    docTree.collectionUUID,
                  ),
                );
              }
            });
            arrayDocNode.children.sort((a, b) => (a.title < b.title ? -1 : 1));
          }
        }

        const docTreeChildrenKeySet = new Set(
          docTree.children.map(({ key }) => key),
        );

        if (!docTreeChildrenKeySet.has(docObj.dxid)) {
          const newDoc = loadRefDoc(docObj, docTree, docTree.collectionUUID);
          if (newDoc) {
            unsortedTreeNodes.push(newDoc);
          }
        }
      }

      if (hasMeasureResult) {
        measureRenderManager.rerenderMeasureResults();
      }
    });

    applyNodesToTree(docTree);
  },
  500,
);

function handleRemoved(removed: string[]) {
  removed.forEach((key) => {
    const refDocNode = refDocNodes.get(key);
    if (refDocNode) {
      userEvents.emit(PRODUCT_EVENT.REMOVE_DOC, refDocNode);
      userEvents.emit(PRODUCT_EVENT.UPDATE_CONSTRAINTS);
    }

    const measurementNode = measurementNodes.get(key);
    if (measurementNode) {
      userEvents.emit(PRODUCT_EVENT.REMOVE_MEASUREMENT, measurementNode);
    }

    // 处理geomBox和geomBox内的对象
    const oldIndex = unsortedTreeNodes.findIndex((n) => n.key === key);

    if (oldIndex !== -1) {
      unsortedTreeNodes.splice(oldIndex, 1);
    }

    if (referenceManager.getObjectById(key) != null) {
      referenceManager.removeObjects([key]);
    }

    const geomBoxNodes = unsortedTreeNodes.filter(
      (n) => n.type === Types.GeomBox,
    );

    geomBoxNodes.forEach((c) => {
      c.children = c.children.filter((_c) => _c.key !== key);
    });
  });

  measureRenderManager.removeMeasureResult(removed);
}

export async function shapeChangePendingHandler(
  {
    added = [],
    shapeChanged = [],
    removed = [],
    visibilityChanged = [],
    propertyChanged = [],
  }: ShapeChangePendingEvent,
  docTree: IAssemblyDocTreeNode & { documentInfo: CadDocument.DocumentInfo },
  setDocTree: Dispatch<SetStateAction<IAssemblyDocTreeNode>>,
) {
  if (!assemblyRendered) {
    return;
  }

  await getDocsInfo(docTree.documentInfo.collectionId);

  if (removed.length) {
    handleRemoved(removed);
  }

  if (added.length) {
    (window as any)?.electronCenter?.emit(CMEVENTS.MODELS_RENDERER_START);
    loadProgressEmit(0);
    loadEmit(true);
    await handleAdded(docTree, added);

    loadEmit(false);
  }

  const changes = Array.from(
    new Set(
      [...shapeChanged, ...visibilityChanged, ...propertyChanged].filter(
        (id) => !added.includes(id),
      ),
    ),
  );

  if (changes.length) {
    (window as any)?.electronCenter?.emit(CMEVENTS.MODELS_RENDERER_START);
    loadProgressEmit(0);
    loadEmit(true);
    await handleUpdate(docTree, changes);

    userEvents.emit(PRODUCT_EVENT.PARTS_RERENDERED);

    loadEmit(false);
  }

  gatherSubPartsOfRootChildren(docTree);

  // trigger the re-render of the tree
  setDocTree(docTree);
  userEvents.emit(PRODUCT_EVENT.ASSEMBLY_TREE_CHANGED);
}
